﻿using System;
using System.Diagnostics.Contracts;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Navigation;
using Liphofra.Core;
using Liphofra.Core.EventAggregation;
using Liphofra.Mvvm.ViewModels;
using Microsoft.Phone.Reactive;

namespace Liphofra.Mvvm.Views
{
    /// <summary>
    /// A phone application page base class replacement that provides all necessary functionality and extension points for the framework.
    /// </summary>
    public abstract class PhoneApplicationPage : Microsoft.Phone.Controls.PhoneApplicationPage
    {
        private bool _isNewPageInstance;
        private IServiceLocator _serviceLocator;

        /// <summary>
        /// Gets the view model base instance, if applicable.
        /// </summary>
        private NavigationViewModelBase ViewModelBase
        {
            get
            {
                return ViewModel as NavigationViewModelBase;
            }
        }

        /// <summary>
        /// Gets or sets the navigation aware view model that is the data context of the page.
        /// </summary>
        protected INavigationViewModel ViewModel
        {
            get
            {
                return DataContext as INavigationViewModel;
            }

            set
            {
                DataContext = value;
            }
        }

        /// <summary>
        /// Gets or sets the service locator.
        /// If no value is set by derived classes, the property is lazy-initialized to <see cref="Liphofra.Core.ServiceLocator.Instance"/>.
        /// </summary>
        protected IServiceLocator ServiceLocator
        {
            get
            {
                if (_serviceLocator == null)
                {
                    _serviceLocator = Core.ServiceLocator.Instance;
                }

                return _serviceLocator;
            }

            set
            {
                _serviceLocator = value;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="PhoneApplicationPage"/> class.
        /// </summary>
        protected PhoneApplicationPage()
        {
            _isNewPageInstance = true;
        }

        /// <summary>
        /// Called just before a page is no longer the active page in a frame.
        /// Important: derived classes need to call the base implementation when they override this method.
        /// </summary>
        /// <param name="e">An object that contains the event data.</param>
        protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
        {
            base.OnNavigatingFrom(e);

            if (e == null)
            {
                return;
            }
            
            if (!e.Cancel)
            {
                HandleFocusedElement();

                // notify the view model
                var vm = ViewModelBase;
                if (vm != null)
                {
                    vm.InternalOnNavigatingFrom(e);

                    switch (e.NavigationMode)
                    {
                        case NavigationMode.Back:
                            OnNavigatingBack(e);
                            vm.InternalOnNavigatingBack(e);
                            break;
                        case NavigationMode.New:
                            OnNavigatingAway(e);
                            vm.InternalOnNavigatingAway(e);
                            break;
                    }
                }
            }
        }

        private static void HandleFocusedElement()
        {
            // this is a convenience workaround: if the currently focused control is a text box or similar control
            // then we update any bindings manually (else it will happen too late in the process
            // and changes then won't be reflected in the view model).
            BindingExpression bindingExpression = null;

            var focusedObject = FocusManager.GetFocusedElement();

            // text box?
            var focusedTextBox = focusedObject as TextBox;
            if (focusedTextBox != null)
            {
                bindingExpression = focusedTextBox.GetBindingExpression(TextBox.TextProperty);
            }

            var focusedPasswordBox = focusedObject as PasswordBox;
            if (focusedPasswordBox != null)
            {
                bindingExpression = focusedPasswordBox.GetBindingExpression(PasswordBox.PasswordProperty);
            }

            // if we got one, update
            if (bindingExpression != null)
            {
                bindingExpression.UpdateSource();
            }
        }

        /// <summary>
        /// Called when a page is no longer the active page in a frame.
        /// Important: derived classes need to call the base implementation when they override this method.
        /// </summary>
        /// <param name="e">An object that contains the event data.</param>
        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);

            if (e == null)
            {
                return;
            }

            var vm = ViewModelBase;
            if (vm != null)
            {
                vm.InternalOnNavigatedFrom(e);

                // determine whether we need to save unsaved data
                switch (e.NavigationMode)
                {
                    case NavigationMode.Back:
                        OnNavigatedBack(e);
                        vm.InternalOnNavigatedBack(e);
                        break;
                    case NavigationMode.New:
                        OnNavigatedAway(e);
                        vm.InternalOnNavigatedAway(e);
                        break;
                }
            }
        }

        /// <summary>
        /// Called when a page becomes the active page in a frame.
        /// Important: derived classes need to call the base implementation when they override this method.
        /// </summary>
        /// <param name="e">An object that contains the event data.</param>
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            Contract.Assume(e != null);

            base.OnNavigatedTo(e);

            if (e == null)
            {
                return;
            }

            var vm = ViewModelBase;
            if (vm != null)
            {
                vm.InternalOnNavigatedTo(e, NavigationContext, _isNewPageInstance);

                // determine whether we need to save unsaved data
                switch (e.NavigationMode)
                {
                    case NavigationMode.New:
                        OnInitialize();
                        vm.InternalOnInitialize();
                        break;
                    case NavigationMode.Back:
                        if (_isNewPageInstance)
                        {
                            OnRestoreState();
                            vm.InternalOnRestoreState();
                        }

                        break;
                    case NavigationMode.Refresh:
                        OnRestoreState();
                        vm.InternalOnRestoreState();
                        break;
                }
            }

            // this indicates whether a new instance has actually been created on the next (back) navigation
            _isNewPageInstance = false;
        }

        #region Protected Extension Points

        /// <summary>
        /// Called when a view should execute initialization logic.
        /// This is typically invoked in a forward navigation, when the view is shown for the first time.
        /// </summary>
        protected virtual void OnInitialize()
        {
        }

        /// <summary>
        /// Called when the view should restore its state.
        /// This is typically invoked in a back navigation, when the view was recreated due to a previous tombstoning event.
        /// </summary>
        protected virtual void OnRestoreState()
        {
        }

        /// <summary>
        /// Called when the view is being navigated away from, back in the navigation stack.
        /// </summary>
        /// <param name="e">The <see cref="NavigatingCancelEventArgs"/> instance containing the event data.</param>
        protected virtual void OnNavigatingBack(NavigatingCancelEventArgs e)
        {
        }

        /// <summary>
        /// Called when the view is being navigated away from, to a new page in the same application, or away from the application.
        /// </summary>
        /// <param name="e">The <see cref="NavigatingCancelEventArgs"/> instance containing the event data.</param>
        protected virtual void OnNavigatingAway(NavigatingCancelEventArgs e)
        {
        }

        /// <summary>
        /// Called when the view has been navigated away from, back in the navigation stack.
        /// </summary>
        /// <param name="e">The <see cref="NavigationEventArgs"/> instance containing the event data.</param>
        protected virtual void OnNavigatedBack(NavigationEventArgs e)
        {
        }

        /// <summary>
        /// Called when the view has been navigated away from, to a new page in the same application, or away from the application.
        /// </summary>
        /// <param name="e">The <see cref="NavigationEventArgs"/> instance containing the event data.</param>
        protected virtual void OnNavigatedAway(NavigationEventArgs e)
        {
        }

        #endregion

        /// <summary>
        /// Registers the view for a loosely coupled event with the event aggregator.
        /// </summary>
        /// <typeparam name="TEvent">The type of the event to register for.</typeparam>
        /// <param name="action">The action to invoke when the event is raised.</param>
        /// <returns>An <see cref="IDisposable"/> instance that can be used to unsubscribe.</returns>
        protected IDisposable RegisterForEvent<TEvent>(Action<TEvent> action)
        {
            var eventAggregator = ServiceLocator.Resolve<IEventAggregator>();
            var theEvent = eventAggregator.GetEvent<TEvent>();
            return theEvent.Subscribe(action);
        }
    }
}